﻿using System;
using System.IO;

using Mono.Cecil.Cil;
using Mono.Cecil.Metadata;
using Mono.Cecil.PE;

namespace Mono.Cecil {

	public enum ReadingMode {
		Immediate = 1,
		Deferred = 2,
	}

	public sealed class ModuleDefinition : ModuleReference, ICustomAttributeProvider {

		internal Image Image;
		internal TypeSystem TypeSystem;
		internal MetadataSystem MetadataSystem;
		internal ReadingMode ReadingMode;
		internal IAssemblyResolver AssemblyResolver;
		internal ISymbolReader SymbolReader;

		MetadataReader reader;

		ModuleKind kind;
		TargetRuntime runtime;
		TargetArchitecture architecture;
		ModuleAttributes attributes;
		string fq_name;
		Guid mvid;

		AssemblyDefinition assembly;
		MethodDefinition entry_point;

		CustomAttributeCollection custom_attributes;
		AssemblyNameReferenceCollection references;
		ModuleReferenceCollection modules;
		ResourceCollection resources;
		TypeDefinitionCollection types;

		public bool IsMain {
			get { return kind != ModuleKind.NetModule; }
		}

		public ModuleKind Kind {
			get { return kind; }
			internal set { kind = value; }
		}

		public TargetRuntime Runtime {
			get { return runtime; }
		}

		public TargetArchitecture Architecture {
			get { return architecture; }
		}

		public ModuleAttributes Attributes {
			get { return attributes; }
		}

		public string FullyQualifiedName {
			get { return fq_name; }
		}

		public Guid Mvid {
			get { return mvid; }
			set { mvid = value; }
		}

		internal bool HasImage {
			get { return Image != null; }
		}

		public AssemblyDefinition Assembly {
			get { return assembly; }
			internal set { assembly = value; }
		}

		public bool HasAssemblyReferences {
			get {
				if (references != null)
					return references.Count > 0;

				if (HasImage)
					return Image.HasTable (Table.AssemblyRef);

				return false;
			}
		}

		public AssemblyNameReferenceCollection AssemblyReferences {
			get {
				if (references != null)
					return references;

				if (HasImage)
					return references = Read (this, (_, reader) => reader.ReadAssemblyReferences ());

				return references = new AssemblyNameReferenceCollection ();
			}
		}

		public bool HasModuleReferences {
			get {
				if (modules != null)
					return modules.Count > 0;

				// FIXME
				if (HasImage)
					return Image.HasTable (Table.ModuleRef);

				return false;
			}
		}

		public ModuleReferenceCollection ModuleReferences {
			get {
				if (modules != null)
					return modules;

				if (HasImage)
					return modules = Read (this, (_, reader) => reader.ReadModuleReferences ());

				return modules = new ModuleReferenceCollection ();
			}
		}

		public bool HasResources {
			get {
				if (resources != null)
					return resources.Count > 0;

				// FIXME
				if (HasImage)
					return Image.HasTable (Table.ManifestResource) || Image.HasTable (Table.File);

				return false;
			}
		}

		public ResourceCollection Resources {
			get {
				if (resources != null)
					return resources;

				if (HasImage)
					return resources = Read (this, (_, reader) => reader.ReadResources ());

				return resources = new ResourceCollection ();
			}
		}

		public bool HasCustomAttributes {
			get {
				if (custom_attributes != null)
					return custom_attributes.Count > 0;

				if (HasImage)
					return Read (this, (module, reader) => reader.HasCustomAttributes (module));

				return false;
			}
		}

		public CustomAttributeCollection CustomAttributes {
			get {
				if (custom_attributes != null)
					return custom_attributes;

				if (HasImage)
					return custom_attributes = Read (this, (module, reader) => reader.ReadCustomAttributes (module));

				return custom_attributes = new CustomAttributeCollection ();
			}
		}

		public bool HasTypes {
			get {
				if (types != null)
					return types.Count > 0;

				if (HasImage)
					return Image.HasTable (Table.TypeDef);

				return false;
			}
		}

		public TypeDefinitionCollection Types {
			get {
				if (types != null)
					return types;

				if (HasImage)
					return types = Read (this, (_, reader) => reader.ReadTypes ());

				return types = new TypeDefinitionCollection (this);
			}
		}

		public MethodDefinition EntryPoint {
			get {
				if (entry_point != null)
					return entry_point;

				if (HasImage)
					return entry_point = Read (this, (_, reader) => reader.ReadEntryPoint ());

				return entry_point = null;
			}
			set { entry_point = value; }
		}

		internal ModuleDefinition ()
		{
			this.TypeSystem = new TypeSystem (this);
			this.MetadataSystem = new MetadataSystem ();
			this.MetadataToken = new MetadataToken (TokenType.Module, 1);
			this.AssemblyResolver = Mono.Cecil.AssemblyResolver.Global;
		}

		internal ModuleDefinition (Image image)
			: this ()
		{
			this.Image = image;
			this.kind = image.Kind;
			this.runtime = image.Runtime;
			this.architecture = image.Architecture;
			this.attributes = image.Attributes;
			this.fq_name = image.FileName;

			this.reader = new MetadataReader (this);
		}

		internal FieldDefinition Resolve (FieldReference field)
		{
			return MetadataResolver.Resolve (AssemblyResolver, field);
		}

		internal MethodDefinition Resolve (MethodReference method)
		{
			return MetadataResolver.Resolve (AssemblyResolver, method);
		}

		internal TypeDefinition Resolve (TypeReference type)
		{
			return MetadataResolver.Resolve (AssemblyResolver, type);
		}

		public IMetadataTokenProvider LookupToken (int token)
		{
			return LookupToken (new MetadataToken ((uint) token));
		}

		public IMetadataTokenProvider LookupToken (MetadataToken token)
		{
			return Read (this, (_, reader) => reader.LookupToken (token));
		}

		internal TRet Read<TItem, TRet> (TItem item, Func<TItem, MetadataReader, TRet> read)
		{
			var position = reader.position;
			var context = reader.context;

			var ret = read (item, reader);

			reader.position = position;
			reader.context = context;

			return ret;
		}

		public static ModuleDefinition ReadModule (string filename)
		{
			return ReadModule (filename, ReadingMode.Deferred);
		}

		public static ModuleDefinition ReadModule (string file, ReadingMode mode)
		{
			VerifyReadingMode (mode);

			return ModuleReader.CreateModuleFrom (ReadImageFromFile (file), mode);
		}

		public static ModuleDefinition ReadModule (Stream stream)
		{
			return ReadModule (stream, ReadingMode.Deferred);
		}

		public static ModuleDefinition ReadModule (Stream stream, ReadingMode mode)
		{
			return ModuleReader.CreateModuleFrom (ReadImage (stream), mode);
		}

		static void VerifyReadingMode (ReadingMode mode)
		{
			if ((mode & ReadingMode.Deferred) != 0 && (mode & ReadingMode.Immediate) != 0)
				throw new ArgumentException ();
		}

		static Image ReadImageFromFile (string file)
		{
			using (var stream = new FileStream (file, FileMode.Open, FileAccess.Read, FileShare.Read))
				return ReadImage (stream);
		}

		static Image ReadImage (Stream stream)
		{
			return ImageReader.ReadImageFrom (stream);
		}

#if !READ_ONLY

		public void Write (string filename)
		{
			Write (CreateFile (filename));
		}

		public void Write (Stream stream)
		{
			using (stream) {
				ModuleWriter.WriteModuleTo (this, stream);
			}
		}

		static Stream CreateFile (string filename)
		{
			return new FileStream (filename, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
		}

#endif

	}
}
